package com.feizhoutuobang.shapeline.service.impl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;
import org.htmlparser.Parser;

import com.feizhoutuobang.shapeline.bo.Article;
import com.feizhoutuobang.shapeline.bo.Feed;
import com.feizhoutuobang.shapeline.bo.Image;
import com.feizhoutuobang.shapeline.bo.impl.ArticleImpl;
import com.feizhoutuobang.shapeline.dao.ArticleDao;
import com.feizhoutuobang.shapeline.dao.FeedDao;
import com.feizhoutuobang.shapeline.dao.ImageDao;
import com.feizhoutuobang.shapeline.service.ConfigService;
import com.feizhoutuobang.shapeline.service.FetchFeedService;
import com.feizhoutuobang.shapeline.util.Calendar;
import com.feizhoutuobang.shapeline.util.Guid;
import com.feizhoutuobang.shapeline.util.StringParser;
import com.feizhoutuobang.shapeline.util.WebClient;
import com.feizhoutuobang.shapeline.util.htmlparser.HtmlModifyingVisitor;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;

public class FetchFeedServiceImpl implements FetchFeedService {
	
	private static final Logger LOG = Logger.getLogger(FetchFeedServiceImpl.class);
	
	private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
	private ArticleDao articleDao;
	private FeedDao feedDao;
	private ImageDao imageDao; 
	private ConfigService configService;
	private WebClient webClient;

	public FeedDao getFeedDao() {
		return feedDao;
	}

	public ArticleDao getArticleDao() {
		return articleDao;
	}

	public void setArticleDao(ArticleDao articleDao) {
		this.articleDao = articleDao;
	}

	public void setFeedDao(FeedDao feedDao) {
		this.feedDao = feedDao;
	}

	public ConfigService getConfigService() {
		return configService;
	}

	public ImageDao getImageDao() {
		return imageDao;
	}

	public void setImageDao(ImageDao imageDao) {
		this.imageDao = imageDao;
	}

	public void setConfigService(ConfigService configService) {
		this.configService = configService;
	}

	public WebClient getWebClient() {
		return webClient;
	}

	public void setWebClient(WebClient webClient) {
		this.webClient = webClient;
	}

	@Override
	public void fetchFeeds() {
		List<Feed> feeds = getFeedDao().getFeedsToFetch();
		for (Feed feed : feeds) {
			try {
				fetchFeed(feed);
			}
			catch (Exception e) {
				LOG.error("error when fetch feed: " + feed.getUrl(), e);
			}
			
			try {
				updateStatistic(feed);
				updateNextFetch(feed);
				getFeedDao().updateFeed(feed);
			}
			catch (Exception e) {
				LOG.error("error when update feed: " + feed.getUrl(), e);
			}
		}
	}

	private void updateNextFetch(Feed feed) {
		int min = getConfigService().getIntegerValue("fetch.extend.min.hours") * 3600;
		int max = getConfigService().getIntegerValue("fetch.extend.max.hours") * 3600;
		int param = getConfigService().getIntegerValue("fetch.extend.param");
		
		int weeklyCount = feed.getStatistic();
		if (weeklyCount <= 0) {
			Date time = Calendar.addSeconds(Calendar.now(), max);
			feed.setNextFetchTime(time);
			return;
		}
		
		int interval = (3600 * 24 * 7) / weeklyCount;
		interval *= param;
		
		if (interval > max) {
			interval = max;
		}
		
		if (interval < min) {
			interval = min;
		}
		
		Date time = Calendar.addSeconds(Calendar.now(), interval);
		feed.setNextFetchTime(time);
	}

	private void updateStatistic(Feed feed) {
		Date now = Calendar.now();
		Date sevenDaysBefore = Calendar.addSeconds(now, -3600 * 24 * 7);
		long oldArticleCount = getArticleDao().getArticleCount(feed, null, sevenDaysBefore);
		if (oldArticleCount == 0) {
			long count = getArticleDao().getArticleCount(feed, sevenDaysBefore, null);
			if (count > 0) {
				Article earlestArticle = getArticleDao().getEarlestArticle(feed, sevenDaysBefore, null);
				Date earlestTime = earlestArticle.getPublishTime();
				long sta = 
					(now.getTime() - sevenDaysBefore.getTime()) * count / (now.getTime() - earlestTime.getTime());
				feed.setStatistic((int)sta);
				//count - 时间：now.getTime() - earlestTime.getTime()
				//？        - 时间：now.getTime() - sevenDaysBefore.getTime(); 
			}
			else {
				feed.setStatistic(0);
			}
		}
		else {
			long weeklyCount = getArticleDao().getArticleCount(feed, sevenDaysBefore, null);
			feed.setStatistic((int)weeklyCount);
		}
	}

	@SuppressWarnings("unchecked")
	private void fetchFeed(Feed feed) throws IllegalArgumentException, FeedException, IOException {
		String url = feed.getUrl();
		LOG.info("fetch feed: " + url);
		
		InputStream stream = null;
		try {
			stream = getWebClient().downloadAsStream(url);
			
			SyndFeedInput input = new SyndFeedInput();
			SyndFeed syndFeed = input.build(new InputStreamReader(stream));
			
			try {getWebClient().release(); } catch (Exception e) {}
			
			feed.setTitle(syndFeed.getTitle());
			feed.setDescription(syndFeed.getDescription());
			feed.setLink(syndFeed.getLink());
			
			List<SyndEntry> entries = syndFeed.getEntries();
			for (SyndEntry entry : entries) {
				try {
					fetchArticle(feed, entry);
				}
				catch (Exception e) {
					LOG.error("error when save file: " + entry.getTitle(), e);
				}
			}
		}
		finally {
			try {stream.close(); } catch (Exception e) {}
		}
	}

	private Article fetchArticle(Feed feed, SyndEntry entry) throws FileNotFoundException, UnsupportedEncodingException, IOException {
		Article article = getArticleDao().getArticleByFeedAndUrl(feed, entry.getLink());
		if (article != null) {
			return article;
		}

		LOG.info("fetch new article: " + entry.getTitle());
		article = new ArticleImpl();
		article.setId(Guid.newId());
		article.setFeed(feed);
		article.setFetchTime(Calendar.now());
		if (entry.getPublishedDate() == null) {
			article.setPublishTime(article.getFetchTime());
		}
		else {
			article.setPublishTime(entry.getPublishedDate());
		}
		article.setTitle(entry.getTitle());
		article.setLink(entry.getLink());
		
		String html = entry.getDescription().getValue();
		html = preProcessHtml(html);
		
		String root = getConfigService().getStringValue("article.store.path");
		String articlePath = root + File.separatorChar + dateFormat.format(article.getFetchTime());
		File articleDir = new File(articlePath);
		if (!articleDir.exists()) {
			articleDir.mkdir();
		}
		
		String imagePath = articlePath + File.separatorChar + "image";
		File imageDir = new File(imagePath);
		if (!imageDir.exists()) {
			imageDir.mkdir();
		}
		
		html = fetchImages(feed, article, html, imagePath);
		html = postProcessHtml(html);
		File content = createContent(article.getId(), articlePath, html);
		article.setContent(content);
		
		getArticleDao().saveArticle(article);
		
		return article;
	}
	
	private String preProcessHtml(String html) {
		html = StringParser.replaceStringIgnoreCase(html, "<strong>", "`strongTagPreserve`");
		html = StringParser.replaceStringIgnoreCase(html, "</strong>", "`strongTagPreserveEnd`");
		html = StringParser.replaceStringIgnoreCase(html, "<b>", "`strongTagPreserve`");
		html = StringParser.replaceStringIgnoreCase(html, "</b>", "`strongTagPreserveEnd`");
		
		return html;
	}

	private String postProcessHtml(String html) {
		html = StringParser.replaceStringIgnoreCase(html, "<p ", "`pTagPreserve` ");
		html = StringParser.replaceStringIgnoreCase(html, "<p>", "`pTagPreserve`>");
		html = StringParser.replaceStringIgnoreCase(html, "</p>", "`pTagPreserveEnd`");
		html = StringParser.replaceStringIgnoreCase(html, "<img", "`imgTagPreserve`");
		html = StringParser.replaceStringIgnoreCase(html, "<br ", "`brTagPreserveSpace`");
		html = StringParser.replaceStringIgnoreCase(html, "<br>", "`brTagPreserve`");
		html = StringParser.replaceStringIgnoreCase(html, "<br/>", "`brTagPreserve`");
		
		html = html.replaceAll("(<[^>]*>)", "");
		
		html = StringParser.replaceStringIgnoreCase(html, "`strongTagPreserve`", "<span class='bold'>");
		html = StringParser.replaceStringIgnoreCase(html, "`strongTagPreserveEnd`", "</span>");
		html = StringParser.replaceStringIgnoreCase(html, "`pTagPreserve`", "<p");
		html = StringParser.replaceStringIgnoreCase(html, "`pTagPreserveEnd`", "</p>");
		html = StringParser.replaceStringIgnoreCase(html, "`imgTagPreserve`", "<img");
		html = StringParser.replaceStringIgnoreCase(html, "`brTagPreserve`", "<br/>");
		html = StringParser.replaceStringIgnoreCase(html, "`brTagPreserveSpace`", "<br ");
		
		return html;
	}

	private String fetchImages(Feed feed, Article article, String html, String imagePath) {
		try {
			HtmlModifyingVisitor visitor = new HtmlModifyingVisitor(feed.getUrl());
			visitor.setWebClient(getWebClient());
			visitor.setImageDao(getImageDao());
			
			boolean colored = getConfigService().getBooleanValue("image.colored");
			int maxImageWidth = getConfigService().getIntegerValue("image.max.width");
			int maxImageHeight = getConfigService().getIntegerValue("image.max.height");
			
			visitor.setImageColored(colored);
			visitor.setImagePath(imagePath);
			visitor.setMaxImageHeight(maxImageHeight);
			visitor.setMaxImageWidth(maxImageWidth);
			
			Parser parser = Parser.createParser(html, "utf-8");
			parser.visitAllNodesWith(visitor);
			
			html = visitor.getModifiedResult();
			
			List<Image> images = visitor.getImages();
			for (Image image : images) {
				getImageDao().addImageOnArticle(article, image);
			}
		} catch (Exception e) {
			LOG.warn("", e);
		}
		return html;
	}

	private File createContent(String id, String articlePath, String html) throws FileNotFoundException, UnsupportedEncodingException, IOException {
		File file = new File(articlePath + File.separatorChar + id + ".html");
		FileOutputStream fos = null;
		OutputStreamWriter osw = null;
		try {
			fos = new FileOutputStream(file);
			osw = new OutputStreamWriter(fos, "utf-8");
			osw.write(html);
			osw.flush();
		} 
		finally {
			try {osw.close(); } catch (Exception e) {}
			try {fos.close(); } catch (Exception e) {}
		}
		return file;
	}

}
